UIEvent
用户对设备的交互行为,最终会在APP中被包装为UIEvent,由对应的APP进行响应和处理,用户的交互行为到UIEvent的过程如下:
- 用户通过硬件产生一个交互,硬件将信息传递给系统;
- 系统的IOKit生成IOHIDEvent,并发送给SpringBoard;
- SpringBoard再通过系统端口将事件传递给对应的APP;
- APP主线程的RunLoop中,由UIKit注册了一个Source1,监听对应端口,接收SpringBoard传递的事件;
- 监听的处理函数中根据传递的事件生成UIEvent,再提交到Application的事件队列中依次处理。
UIEvent有不同的类型,主要有以下几类:
- touches:屏幕的触控事件;
- motion:陀螺仪对应的时间,晃动(motionShake)是其子类型(subtype);
- remoteControl:遥控器(tvOS),耳机等产生的遥控时间;
- presses:物理按键(物理Home键等)的按压事件。
2020-05补充:
iOS13.4之后UIEvent新增了scroll、hover和transform类型,对应设备新的交互方式。
UIRespnder
UIResponder是UIKit中的一个抽象类,它有一个nextResponder属性指向下一个UIResponder,UIKit通过UIResponder实现了职责链模式,用于处理和传递UIEvent,iOS中将UIResponder实际子类的实例称为Responder(响应者),将Responder所构成的职责链称为Responder Chain(响应链),当事件被传递给Responder,Responder可以对事件做出响应,如果Responder不做处理,则默认将事件沿Responder Chain继续传递,直至被处理或最终被忽略。
Responder Chain
UIKit中的UIResponder的实际类型为UIView、UIViewController、UIApplication和AppDelegate:
- 子View的nextResponder是其superView;
- View如果是ViewConroller的根视图,则它的nextResponder是其ViewController;
- ViewController的nextResponder是其parentViewController;
- ViewController如果是Window的根ViewController, 则其nextResponder是Window;
- Window的nextResponder是Application;
- Application的nextResponder是AppDelegate;
Tip:实测touches类型以外的UIEvent在传递给Application之后,不会再继续向下传递。
UITouch
不同类型的UIEvent,Application在传递给Responder Chain时有不同的处理,touches类型以外的UIEvent会直接交给FirstResponder处理或传递,touches事件则会先交给KeyWindow,再由KeyWindow通过Hit-Test找到Hit-Test View(触碰视图),然后将touches事件以及事件的UITouch对象(可能是多个)传递给Hit-Test View。
Hit-Test
Hit-Test是一个嵌套循环调用-hitTest:withEvent:
方法的遍历过程,大致流程如下:
- Application调用KeyWindow的
-hitTest:withEvent:
方法; - 方法中先判断自己能否接收touches事件(userInteractionEnabled/hidden/alpha),不能就直接返回nil;
- 再通过
-pointInside:withEvent:
方法判断触摸点是否在自己区域内,不在就直接返回nil; - 倒序循环遍历subViews,嵌套调用subView的
-hitTest:withEvent:
方法,直到找到Hit-Test View; - 当2、3条件都满足,subViews中也没有找到合适的Hit-Test View时,就返回View自己;
- Applicaton将touches事件和以及事件的UITouch对象传递给Hit-Test View。
Tip:可以通过覆写
-hitTest:withEvent:
和-pointInside:withEvent:
方法来改变APP对touches事件的响应逻辑,如改变响应对象,改变触摸响应区域等。
touches事件传递给Hit-Test View之后,如果没有被识别为UIAction,也未被识别为手势,则会沿着响应链进行传递,链上的响应者通过覆写touches的传递方法,都有机会对事件作出响应,touches的传递有以下五个相关方法:
1 | // Generally, all responders which do custom touch handling should override all four of these methods. |
Tips:
- 覆写touches的传递方法时,如果需要继续传递,应该调用super的对应方法,而不是直接调用nextResponder的对应方法;
- 单点触控时UIEvent对应一个UITouch对象,多点触控则对应多个UITouch对象;
- 一个touches事件传递时,方法传递的UIEvent和UITouch对象是同一个(组),最后的Responder的Ended方法调用之后才销毁。
UIAction
View在收到touches事件后,在经响应链传递前,还需要先做两个判断:
- 判断View是否为UIControl的子类:如果是,则对touches事件做UIAction识别,识别为UIAction则通过UIControl的Tareget-Action模式进行处理,touches事件不再经响应链传递;
- 倒序查找View所在的视图层级是否添加了UIGestureRecognizer:如果有添加,则对touches时间做手势识别,如果被识别为手势,这通过手势的Target-Action模式进行处理,touches事件默认不再经响应链传递。
UIControl中针对UIAction的识别有四个跟踪方法,如果想自定义跟踪识别,可以覆写相关的方法:
1 | - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; |
Target-Action
当UIControl将touches时间识别为UIAction之后,通过Target-Action模式交个target进行处理的过程如下:
- 创建UIAction,调用UIControl自己的
-sendAction:to:forEvent:
方法,将Action转发个UIApplication对象; - UIApplication对象调用
-sendAction:to:fromSender:forEvent:
方法,将Action分发给指定的target。
Tip:UIControl的Action在设置Target时,可以设置为nil,这样UIAction会沿着响应链寻找Target,据此可以在运行时决定Action的Target,但除非特殊需求,不建议滥用此特性。